✨ Evaluate generated XMD text with constrained authority (#713) - #756
Conversation
`<Plan>` produces a program and never runs it. `<Evaluate>` is now the
composition site where one runs: paired content renders a producer into a
private program buffer, `program={value}` supplies complete-program source
directly, and both admit a complete root and evaluate it in the current
execution — its lifecycle, journal, cancellation scope, providers, working
directory and the authority in force where the element was written.
The forms are disjoint and ambiguity refuses before program content is produced
and before any program effect runs. The root deciding what happens is the
program: its frontmatter, props schema, `returns` and `<Output>` selection all
apply, root props are the explicit `props` object rather than the caller's, and
a value root requires `as`.
Complete programs record their own `evaluate_program` durable event; the
restricted `generated_xmd` admission #369 delivered is unchanged and
complete-program support is not reachable through `source` or `allow`. A
partial continuation expands the retained source and restores completed nested
effects; a changed program at the same occurrence is stale input and neither
source runs.
Canonical execution answers the new `Component.expandProgram()` from the frame
it already holds, which is what makes site authority a fact about the frame
rather than something a prop, binding or middleware return value could supply —
and what keeps a producer's private closure from crossing into the program.
| "Component.expandProgram() has no provider: not inside a function component invocation.", | ||
| ); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| yield* provideEnv(programEnvironment(site.callerValues, program.props)); | ||
| if (program.returns !== undefined) { | ||
| // A value root has no rendered result to fall back on, so an undecided | ||
| // error is the evaluation's failure rather than text in the document. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // error is the evaluation's failure rather than text in the document. |
| } | ||
| const mode = definition.returns === undefined ? "text" : "value"; | ||
| // Before the props are looked at, because a value root with nowhere to put | ||
| // its result is refused whatever its props would have validated to. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // its result is refused whatever its props would have validated to. |
| // Which grammar this element is written in, decided from the props alone. | ||
| // Neither half of it reads the authored form, so both are settled before | ||
| // the durable name is claimed: a combination this host does not have is not | ||
| // a fragment or a program being refused. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // a fragment or a program being refused. |
|
|
||
| // After the claim, so the shape read here is the one the claim proved this | ||
| // element was written in. A borrowed invocation reports somebody else's | ||
| // element, and refusing it is the claimant's answer rather than this one. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // element, and refusing it is the claimant's answer rather than this one. |
| origin: ORIGIN, | ||
| // The run profile's sentence plus the one form only this profile has. | ||
| // Stated here rather than in core because a catalog must not advertise a | ||
| // prop the profile it describes would refuse. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // prop the profile it describes would refuse. |
|
Warning This PR changes release configuration (scripts/build-npm.ts) without touching PR #756: ✨ Evaluate generated XMD text with constrained authority (#713)158 files, +23774 / -1906 Scope🔴 PR has 25680 lines changed. Split into focused PRs. 🟡 25680 lines changed. PRs under 400 receive more thorough review. 🟡 158 files changed. Are all changes related? 🟡 Changes span 25 directories. 🟡 PR mixes config and source changes. 🟡 New abstraction files: packages/core/tests/support/answer-provider.ts. Verify 3+ consumers. 🟡 package.json changed without dependency justification. Structural🟡 2 empty function bodies. Oxlint structural signals:
Slop
Oxlint slop signals:
Static AnalysisOxlint: 202 diagnostics across 47 files (26 rules) no-unused-vars (34): packages/core/src/components/Evaluate.ts, packages/core/src/agent/components.ts, packages/core/src/expand.ts (+10) CorrectnessFILE: packages/core/src/components/Evaluate.ts FILE: packages/core/src/expand.ts FILE: packages/core/src/fragment-capabilities.ts FILE: packages/core/src/agent/components.ts FILE: packages/core/src/inspect.ts FILE: packages/core/src/execute.ts FILE: packages/core/src/definition.ts FILE: packages/core/src/syntax-reference.ts FILE: packages/core/src/evaluation-profile.ts FILE: packages/core/src/expand.ts FILE: packages/core/src/execute.ts FILE: packages/core/src/definition.ts FILE: packages/core/src/expand.ts FILE: packages/core/src/execute.ts FILE: packages/core/src/expand.ts FILE: packages/core/src/expand.ts FILE: packages/core/src/expand.ts |
The `evaluate_program` record is hostile data and was read as though it were not. It is now two closed shapes — admitted and refused — with every nested record closed too, so a missing, additional or misspelled member at any depth is a record this evaluation did not write. Shape is not enough, because every member of the record is a claim about another one. Before anything expands, the retained source must hash to the retained digest, reparsing it must produce the retained root mode and a valid body structure, the retained elements must be the ones that source writes in that order and those forms, and the supplied props must validate to the retained validated props. A record failing any of them is refused as unreadable and neither the retained nor the current program performs an effect. The admission also retains what each name the program writes resolved to at this site, which restores the current-site compatibility contract: a continuation whose site now answers one of those names with a different identity, or admits it in a different form, refuses before either implementation runs, while an unchanged site resumes. Resolution stays behind canonical execution. The execution builds a resolver, carries it on the expansion authority, and settles the comparison itself before the first program effect. `Component.resolveProgramSite()` lets the admission retain what the site answered; nothing rests on that answer, because the canonical comparison is what decides — so short-circuiting it refuses the evaluation rather than widening it. `ExpansionAuthority` is still reachable from no component, and no prop, binding, middleware result or public context state supplies authority.
#713) The retained identity came from an independent `selectComponent()` call, which is not what runs. `Component.importComponent` middleware may answer without delegating or replace what came back, so a second resolution described a definition nobody invokes: two different middleware answers retained one identity, compared equal, and a continuation ran the other one. Resolution now goes through the ordinary chain and takes the identity from the final answer. A canonical tier keeps its canonical identity. An answer an identified provider supplied keeps that provider's own — a stable origin, its key for the name, and a revision that changes with the implementation — stated at the installation boundary through `useImportProvider()`, where canonical execution mints a claimant for that execution and binds the terms to the exact answer in execution-private state. Never on the definition, which an answer can copy; never through a replaceable Context answer. An answer nobody identified still works for ordinary expansion and cannot back a durable grant: a continuation would have nothing to compare. Two live providers under one origin refuse, and so does a second claim on one answer. Identities are closed tagged records now, so a missing, empty, additional or mis-tagged member is unreadable like every other corrupted member. The comparison and the invocation are one decision: canonical execution keeps its own copy of each witnessed answer and authorizes the program's imports against it, so an answer that changed between the check and the use is refused where it would be invoked rather than silently preferred.
…ts import (#713) Three corrections. **Closed authority was overwritten.** The program authority replaced the site's `imports` with one that forgot its tiers, and resolution asked the chain without applying them, so an identified provider could replace a bundled or declared component and be admitted as its identity. Resolution now applies the site's closed authority exactly as ordinary expansion does, and the program's expansion keeps the tiers it already had. A provider-supplied answer is witnessed rather than issued, so it authorizes nothing for a name a tier closed — the guarantee that sharing one witness table had quietly broken. **Resolution now journals nothing.** The selection half of an import and the definition it produces are separated from the durable operation, so a resolution-only path settles the site without recording anything. `evaluate_program` commits before the program's own ordinary import. **Expansion no longer asks again.** The answers that passed reconciliation reach expansion as settled answers: an element whose name is among them does not enter `Component.importComponent` at all. The chain is entered twice for one evaluation, never a third time. The authored element still records one ordinary import after the admission, restored from the already-authorized answer with identity-domain and form selection intact. Provider identity is read exactly once at registration and closed over as copied primitives, so a getter cannot answer the duplicate check and the claim differently and a later edit changes nothing. Structural syntax resolves directly to its structural identity: no lookup, no provider, no import record.
The settlement map was keyed by name, so a program writing `<Open /><Open />` collapsed two independently resolved answers into one and invoked the first twice. Each occurrence now carries its own settlement, keyed by where the element was written and paired with the admission's retained order, so two elements resolved to two implementations invoke those two in order. An unresolved occurrence is a settled outcome as well. It was absent from the map, so the element fell through to the ordinary open chain and could be answered by a lookup reconciliation never made. It now reports the ordinary unresolved failure without consulting anybody, and the chain is entered exactly twice for one evaluation whatever an occurrence settled to. Settlements belong to the program's own parsed body. A component the program invokes expands its own bytes at its own offsets, which nothing reconciled, so carrying them in would have made a settlement a name-wide override of somebody else's element. The nested durable record is a protocol rather than a marker. A settled occurrence records exactly `settled` and `name`, and a continuation parses that as the hostile replay data it is before invoking anything: a missing, additional, mistyped or unknown member, and a record naming another component, each refuse. A refusal this boundary raises during resolution — a second provider claiming one answer, an answer changed after it was claimed — now reaches the caller instead of being read as "this name resolves to nothing".
`readSettledImport()` cast the restored value and inspected it directly, so a value that answers differently on a second read decided what a continuation invoked. It now detaches the value into plain JSON under a failure boundary and inspects only that copy: a member read once cannot answer twice, and a value that will not detach is simply not the record. The admission's own restored result is detached under the same boundary for the same reason. The prose said one settled `import_component` per settled occurrence. It is one per *resolved* occurrence: an unresolved one loads no component and records no such event, which PE41 now asserts. A retained value that refuses to be read at all — a Proxy trapping `ownKeys` or a descriptor, a throwing accessor, a cycle — never reaches this parser. The run's own retention check reads every retained result before the document body starts and refuses there, in the journal's own words. That is a persistence boundary rather than this one, so the evidence proves what this boundary can promise: the component is never invoked.
…mand (#713) EP9 proves a workflow run is given one `<Evaluate>` and not two, and it proves it by starting a real run. `xmd workflow` exists on the Deno entrypoints alone, so under Node and Bun the command refused before a run existed and the case asserted nothing — which is why `test-node (2/7)` and `test-bun (3/4)` failed while the same file passed locally under Deno. The case moves to its own file and joins the Deno-only exclusions with its reason and issue, the way every other test of a workflow command does. Tier EP's portable half stays where it was and still runs on all three runtimes.
| "Component.expandProgram() has no provider: not inside a function component invocation.", | ||
| ); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| "Component.resolveProgramSite() has no provider: not inside a function component invocation.", | ||
| ); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| offered?.close(); | ||
| record = yield createDurableOperation<Json>( | ||
| { type: "import_component", name, ...sourceDescription(position) }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| case "workflow": | ||
| // The exact pinned source, already in hand: the bundle was read from | ||
| // the definition's own commit before this run existed, so recording it | ||
| // reads nothing and a replay reconstructs it without resolving a name. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // reads nothing and a replay reconstructs it without resolving a name. |
| case "declared-markdown": | ||
| // The exact declared bytes, already in hand: they were admitted | ||
| // before this run imported a root, so recording them reads nothing | ||
| // and a replay reconstructs the component without resolving a name. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // and a replay reconstructs the component without resolving a name. |
| digest: selected.digest, | ||
| content: selected.source, | ||
| // Recorded only when it holds, so an ordinary declaration's record | ||
| // is exactly what it always was. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // is exactly what it always was. |
|
|
||
| // Parsed rather than asserted: a replay hands back whatever the journal holds, | ||
| // and a history somebody else wrote is not a `DurableSelection` because it | ||
| // type-checked on the way in. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // type-checked on the way in. |
| } | ||
| // Canonical execution's own answer keeps its canonical identity. The | ||
| // selection that produced it is what names it, and asking for it here | ||
| // describes the definition this import actually settled on. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // describes the definition this import actually settled on. |
| // This import is canonical execution's own answer for a name this | ||
| // execution closed, which is the only provenance exact source is read | ||
| // from. An open import — one no tier claims — never sets it, however its | ||
| // answer describes itself. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // answer describes itself. |
An event listener an Effection operation installs is state that operation owns, but nothing about an event source enforces that. A handler attached to a socket, a child process or a DOM target survived the operation that attached it, and kept writing to state the abandoned owner left behind. Cleanup that runs when the event fires is not cleanup: a cancelled wait is exactly the case where the event never arrives. `@effectionx/node` moves to 0.2.5, whose `once()` registers nothing until it is interpreted and detaches in a `finally` — after delivery, a halt, or losing a race. Every direct pin moves together; `@effectionx/process` stays at 0.8.1, which leaves one audited transitive 0.2.4 lock entry that nothing here resolves. `local/require-scope-bound-event-registration` makes the policy a blocking gate. It resolves sources, owners, `action` and `ensure` through their imports rather than their spelling, and an owner is the nearest generator or `action()` executor and nothing wider. Cleanup counts as established only when it is in place before the thing it releases exists: a lexical `finally` entered before the subscription, an `ensure()` that *completed* before it, or the cleanup an `action()` returns. An `ensure()` yielded afterwards is rejected, because entering it is itself a suspension — an owner halted there unwinds with no cleanup registered at all, measured, with the listener still attached. Every applicable registration is migrated onto one of those shapes. The three owners that own a child process register their cleanup before spawning it, and that cleanup keeps every handler attached until the child's `close` — the only event that says the process and the pipes this run inherited are finished. An assigned exit status is not that boundary, so none of them reads one: a cancelled command still captures what its child wrote on the way out, and still escalates SIGTERM to SIGKILL exactly as before. Readiness races are interpreted inline, in the same synchronous run as the `connect`, `listen` or `spawn` they watch, because a spawned race attaches its arms a turn later and the source can settle in that turn. The foreground reaper names its exit handler and releases it in the one funnel every settlement passes through. `nodeResponseChannel` becomes an `Operation`, so both of its handlers belong to the request task. The form responder keeps its request observed until that request's own `close`, so the peer's hang-up reaches a listener instead of the process. Web server teardown detaches every accepted socket's close handler before it suspends, then ends request tasks before destroying their connections. The workflow and CLI HTTP fixtures read request bodies in tasks of the server's own scope. Every distinct manual owner is cancelled while its listeners are live. Each case measures the baseline the runtime was already holding, reads the vector once with the owner running, halts it before the event that would have settled it, reads the vector again before replaying anything, then replays and proves no capture, callback or result moved. Where a source is a child process or accepted socket an operation does not hand out, a package-private observation seam carries it; no public package contract widened. This is a lifecycle change. Event ordering, output aggregation, response bytes, failure precedence, process signalling and every public result are unchanged.
<Evaluate> evaluate complete XMD programs (#713)|
The author contract for #713 has been superseded and the issue/PR handoff is now amended. Please do not continue or merge the current complete-program implementation. Correction is sequenced after #759: use its exact merged result as the base, retain the durable replay/isolation work named in this PR body, remove the complete-root machinery, and implement the unified fragment-text contract. The next review requires a new signed feedback commit and the focused evidence listed here; the current |
* ♻️ Separate Plan syntax observation (#758) * ✨ Make `<Syntax />` a public protected component (#758) What a document may write here is a public question, and this makes the answer public too. `<Syntax />` is now a component canonical core owns, available throughout XMD: it renders the catalog for the site it is written at, in the Markdown `xmd syntax` prints, from one construction and one renderer — so an operator printing a profile and an agent being told what to write are never given different accounts of one environment. A new canonical protected tier sits after structural syntax and ahead of every host or author tier. It is the resolver's own table rather than a registration, so a repository `Syntax.md`, a bundled `Syntax`, an ordinary or reserved registration and a host's declared Markdown are each refused where the claim is made, and import middleware may observe, delegate or refuse the import without answering one. Canonical execution carries the catalog lexically on its own expansion authority, built from the selection inputs it captured before any installation, middleware or document code ran — or from the one catalog a trusted host stated for its profile. Each occurrence claims the identity the execution minted, observes once, and retains exactly `{ catalog }`; a continuation hostile-parses that record and restores what the run actually showed. `<Plan>` returns to five private capabilities. The packaged bytes write the same public `<Syntax as="syntax" />` any document writes, and `xmd plan` states the `run` profile's catalog at the execution boundary rather than through Plan's private assembly. * 🏷️ Give protected and bundled components their own catalog origins (#758) Two entries in the catalog said something untrue, each by borrowing a neighbouring origin kind. `<Syntax>` reported as `{ kind: "registered", reserved: true }`, and rendered as "reserved registration". A reserved registration is a *host* installing something under a name it wants kept: it can be absent from another run, replaced by a different host, or refused when two hosts claim it. None of that is true of a name canonical core owns, so a reader deciding whether they could supply `Syntax` themselves got exactly the wrong answer. It now reports `{ kind: "protected", origin }` and renders as "protected component", in the structured entry, the rendered Markdown and `inspectComponent` alike. A workflow-bundle member reported as `{ kind: "repository", path }`, which reads as a file the reader could edit. It is the exact blob `sourceHash` names, fixed when the run was defined. It now reports `{ kind: "workflow", path, sourceHash }` and renders the abbreviated object id beside the path. Category placement keyed off the `repository` kind, so the new kind is named there too — a bundle member is the run author's own Markdown and stays under user-provided. Both are additions to a closed set, so the catalog is version 2 rather than a silent widening of version 1: a version-1 reader was promised those origins were all of them, and neither emitting an unknown kind nor reusing a neighbour keeps that promise. Two durable shapes are now documented rather than merely implemented. `syntax_catalog` joins the journal effect table with its exact name and closed `{ catalog: string }` payload, and the `import_component` protocol gains `{ kind: "protected" }` — closed on that one member, because a protected component has no path, no origin to look up and no implementation to serialize. Its replay behavior is stated: the running execution supplies the implementation it built, and an execution that built none refuses rather than resolving the name again, which would run whatever is offered under that name today. The evidence tier moves off `SC` and `SL`, which already name Sample component and Own-scope context updates, to `SYN`. SY19, SY20 and the Evaluate clause of SY21 move to #713. They describe what `<Evaluate>` does with a narrower catalog, and #759 installs one nowhere; worse, `<Syntax>` is not in the generated-XMD pinned identity table, so it cannot be invoked inside a fragment until #713 admits it. What #759 owes is the seam, and SYN25b proves it: a fixed narrower observation answers with exactly the catalog it was handed and adds nothing of its own. * ✨ Let `<Syntax>` render selected components' documentation (#758) The compact catalog answers *what may I write here*. It does not answer *how do I use this one*, and an agent handed seventy entries to explain one component has to guess. So `<Syntax>` gains an optional `names`: <Syntax names={["Elicit", "File"]} /> which renders each selected component's catalog metadata followed by its long-form documentation, once each, in catalog order whatever order they were asked for in. `as` captures the same text. The bare form is unchanged. The documentation is a package's own. A registration bundle keeps `components.md` beside the boundary it documents, located from the owning module's URL — never the working directory and never `--include`, because a documentation set that moved with the caller would describe a different product depending on where somebody stood. `deno compile --include` embeds it, the npm build copies it, and JSR publishes the source, so all four distributions load the same bytes. The index validates rather than trusts. A level-two heading is an exact component name, text before the first documents the bundle, and level-three and deeper stay in the section they are written in so a component's own documentation can have structure. A heading that names something the package does not supply, one that appears twice, and one that is not a component name at all each refuse the whole index. Headings inside fenced blocks are examples, not sections. A component with no section is legal and renders the sentence #758 states for one. Documentation joins to metadata by name *and* owning package, so a repository `Elicit.md` never receives the built-in's prose: it has a repository origin, which names no package, and the join has nothing to match on. The observation carries two inputs now, and that is why it is an object rather than a string. *What may I write* and *what may I read about* are different questions, and a narrowing evaluation boundary answers them differently on purpose: selection reads the enclosing authoring catalog, so a nested author can be told how `<Elicit>` works where they may not run one, and every rendered entry states whether it is available in the current evaluation. #713 installs that boundary; this proves the seam. `xmd syntax Elicit` is the same lookup — one selection, one index, one renderer — so the command and the component cannot describe one component two ways. The compact catalog and version-2 `--json` are untouched; documentation is prose rather than a catalog member. Also reconciles the CLI help's stale version-1 claim. * 🔍 Validate the documentation index against the package, not the catalog (#758) The index checked its headings against whichever catalog was in scope. Those are different sets, and conflating them is a real bug: a narrowing evaluation boundary carries a catalog holding a handful of admitted components, so validating core's own `components.md` against it reported `Elicit` as something `@executablemd/core` does not supply — and the two-input seam refused to build its index at all. A heading has to name a component this *build* ships; which of them a given site can select is what the selection answers. The known set is now core's own registrations plus the protected tier, read from the same declarations execution reads. Adds the evidence the amended contract asks for: - the narrowing seam, proved without an `<Evaluate>`: a narrowed observation reports the narrowed vocabulary bare, documents the enclosing catalog by name, and marks availability truthfully in both directions; - named retention — the occurrence retains its final rendered text, a continuation restores it without rereading documentation, a corrupted record refuses; - the index itself: bundle prose and per-component sections, deeper headings kept inside their section, a fenced heading read as the example it is, and refusals for a duplicate section, a heading that is not a component name, a heading naming something the package does not supply, and one component documented twice; - the join, positively and negatively: a registration and a protected component both receive the package's prose, a repository replacement of the same name receives none; - `xmd syntax Elicit` equivalence, with the compact catalog and an unknown name as controls; - the compiled binary documenting a component from a directory that is not the checkout, which is what proves the embedded asset rather than the entry. The new test file runs under Deno, Node and Bun, so it joins all three shards rather than needing an exclusion. Reconciles the remaining stale version-1 claims: the `SyntaxCatalog` paragraph and the `xmd syntax` row in architecture.md, and the Syntax Markdown suite. * 🩹 Compare component identities, not names, when reporting availability (#758) Four corrections from review, and one of them was a real hole. **Availability compared spelling.** The named form selects its reference entry from the enclosing authoring catalog and marks it available if the current evaluation can run it — but it asked whether anything *called* that could run. The whole point of the two inputs is that the enclosing catalog may hold a different component under the same name, so a reference entry for the built-in `Elicit` beside an admitted repository `Elicit.md` reported the built-in as available: an author told they may execute the thing they were just shown. Availability now compares the complete identity — name and every member of the origin, so a workflow blob differs from another by `sourceHash` and a declared component by `digest` — with negative controls for each way two components can share a name. **The heading grammar excluded namespaced components.** A private regex accepted one capitalised segment, so `File.Delete`, `Session.Launch` and `PullRequest.Reviews` could not be documented or looked up at all. It now uses the canonical `isComponentName()` rather than a second copy of the rule. **Fences closed on the wrong condition.** An example written in four backticks containing a three-backtick block ended at the inner one, so everything after it was read as documentation and a `## Heading` in the example started a section. A fence now closes only on the same character at least as long as the one that opened it. **Shared core read the filesystem directly.** `node:fs/promises` is replaced by the host filesystem operation the root document's own read goes through — not the document-facing `Files` authority, since this is the engine reading its own package. Resolution stays package-relative. Also adds the multi-source assembly the complete index needs: contributions are supplied by trusted host installation, each naming its file and the components it must account for, rather than the index being hardcoded to core. And a Markdown component's own document is now readable as its long-form documentation, since a repository, bundled or declared component belongs to the run rather than to a package and no `components.md` documents it. Reconciles the last version-1 claim in Tier SX and adds its named-lookup row. * 📚 Document every component core supplies, and protect the assets (#758) **The read was reachable from a document.** Last round I moved the package asset read onto the runtime `readTextFile`, reasoning it was the trusted path the root document's own read uses. It is `API.Fs`, which a running document can compose around — so a repository component, an eval block or an installed handler could answer the read and decide what the product says about itself. It now goes through the direct Effection filesystem, which no document-scoped middleware sits in front of. SYN40 plants `API.Fs` middleware around a repository component that wraps the named form and proves the canonical prose survives and the read never reaches that Api; routing it back through `API.Fs` fails that case. **Coverage is exact.** A first-party package now documents every component it supplies: a missing section refuses the whole index, as an unknown heading and a duplicate already did. A partially documented package is not a valid build, because a reader cannot tell an undocumented component from one with nothing to say. SYN38 is replaced accordingly — deleting any one built-in's section fails, with a fully covered package as the positive control. So core is complete: the ten components it registered without documentation, and the seven Agent components, each with a section written against its actual contract rather than its catalog line. The Agent boundary keeps its own `components.md` beside `agent/components.ts`. **The assembly is real.** `DocumentationContribution` was scaffolding nothing used; the run profile now contributes the Agent boundary beside the registry it installs, from the same declarations, captured before any document code exists. Sets merge per owner rather than replacing — core has two boundaries under one origin, and keying by owner alone made the second hide the first, which is what made `xmd syntax Elicit` refuse until it was fixed. **Three parser corrections.** The heading grammar is `isComponentName()` rather than a private regex, so `File.Delete` — a real core component — can be documented at all. A closing fence must be the same character, at least as long as the opener, and carry only trailing whitespace, so a same-length delimiter followed by text stays inside a longer example. Markdown-backed documentation is split by the canonical frontmatter parser rather than a delimiter search. **`names` selects components.** A structural construct is not one, so `<Syntax names={["If"]} />` refuses; the fallback sentence is proved with an undocumented repository component, which is the case it is actually for. Both assets ship through source, npm and compiled layouts. * 🔗 Carry package documentation through the execution boundary (#758) `xmd syntax Prompt` printed Prompt's documentation while `<Syntax names={["Prompt"]} />` answered with the fallback sentence. One product, two answers to one question, and the fallback was the wrong one: the command assembled the profile's contributions and the component reached a core-only index, because `documentationIndexFor()` defaulted to none and only the command passed anything. The contributions now travel the way the catalog beside them does — captured at the installation boundary before any document code exists, carried by value on `ExecutionInstallation.documentation`, and handed to the root observation. Several are ordinary rather than refused, unlike the single catalog: one registration boundary is one file, and a profile installing four packages has four. `fixedCatalogObservation` takes them too, so narrowing what may *execute* does not narrow what an author may read about — #713 still installs the executable catalog. Three documentation errors, each caught against the registration rather than by rereading the prose: - `<Glob>` takes `include={["docs/**/*.md"]}`, a list of patterns, not a `pattern` string; - `<CodeBlock>` is self-closing with `value={…}`, and chooses a fence the value cannot break out of; - `<Test>` runs under `xmd test` *or* inside `<Testing>`, not only the latter. That is the reviewer's point about prose review not being verification, and it is well taken: all three read plausibly and all three were wrong. * 📖 Document every first-party component, and gate the build on it (#758) Five registration boundaries now each own a `components.md` beside themselves, with a contribution derived from the declarations that boundary registers rather than a list somebody maintains: - core's own components, and its Agent registrations; - the CLI's `<Verbose>`; - testing — `<Testing>`, `<AssertThrows>`, the fourteen assertions, and the five execution-harness components; - web's `<WebForm>`; - all thirteen repository-composition components, `<Dir>` included — the one registered from a definition rather than spelled inline, and so the easiest to miss if the set were hand-written. Each section is written against the component's actual declaration: its forms, its props, whether `as` is required, what activates it, and what it refuses. The last round shipped three examples that read plausibly and were wrong, so this round every example was checked against the registration it documents rather than against my memory of it. **A build cannot produce a distribution without the complete index.** `scripts/validate-documentation.ts` runs the same assembly the `run` profile does, and `deno task build` and `build-npm.ts` both pass through it. Copying the assets was never the check: a package built from a drifted set installs cleanly and refuses the first time somebody asks it for documentation. Each failure class is mutation-tested against real shipped files — a deleted section, an unknown heading, a duplicated section, and drift in a package outside core. Wiring the boundaries immediately found the next gap: four of the six assets were absent from the compiled `--include` list and the npm copy list, so the binary resolved `xmd syntax Git.Commit` to a missing-asset error. All six now ship through compiled, npm and JSR — the JSR dry run lists every one. Also adds the cancellation case: a named observation halted mid-flight completes its teardown and commits no successful `syntax_catalog` record. And the compiled probe now compares its output byte-for-byte with the source CLI for `Git.Commit`, a component outside core's own documentation file, so the comparison exercises a second copied asset path rather than re-proving the first. * 🪟 Derive a narrowed observation from the enclosing one (#758) The narrowing seam required an evaluator to hold the raw documentation contributions and hand them back to build a narrowed observation. That list is execution-private for a reason, and rebuilding an index from it is how two indexes drift apart. `CatalogObservation` now derives its own: observation.narrow(admitted) What comes back reports the admitted vocabulary from `observe()` and keeps *this* observation's authoring catalog and documentation index for `document()`. #713 needs the admitted catalog and nothing else. Proved by breaking it: a narrowed observation that drops the enclosing reference fails SYN25e and nothing else. Contributions are snapshotted field by field at the boundary — owner, asset and text copied, the name set materialized into one this module owns. A contribution is a caller's object: its array can be reordered, its source replaced, its `Set` added to after capture, and an iterable can answer differently the second time it is walked. SYN25f mutates all of those after capture and proves the observed documentation and coverage are the captured values. SYN46 now proves cancellation *reached* the named documentation work rather than arriving before it: the observation records entry, then teardown, in that order, and a cancellation that never got inside would leave the first marker absent. It also states the mechanism correctly — a durable operation records its event on completion, so a cancelled one commits nothing at all rather than committing a failure. SX17 compares the complete rendered output of `xmd syntax Prompt` and `<Syntax names={["Prompt"]} />` rather than phrases from it. Substring agreement would pass just as happily if one surface kept the heading and silently dropped the documentation. Prose reconciled with what the code enforces: a first-party component with no section refuses the index, and the no-documentation sentence is for a custom component. `documentation-index.ts` and `architecture.md` said the opposite. * 🔒 Snapshot documentation at the installation boundary, and close the distributions (#758) **The snapshot was taken too late.** It happened inside `rootCatalogObservation()`, which runs long after `install()`. So an installation could hand over a contribution, then rewrite its own source text and name set from inside its own `install()` operation, and a document would be told whatever it changed them to. The capture moves to `runInvocation()`, before any `install()` runs, and only the captured value travels onward. SYN25g proves it end to end through `executeInstalled()`: an installation that mutates its source, owner and `Set` from `install()` gets the pre-installation snapshot back. It fails against 2e1ba28. **Cancellation now reaches the documentation work.** SYN46 records entry into the observation and its teardown, in that order — a cancellation arriving before the work would leave the first marker absent — and states the mechanism correctly: a durable operation records on completion, so a cancelled one commits nothing rather than committing a failure. **The npm boundary was not what I said it was.** I reported it as an environment fault after a `spawn sh ENOENT`. Reproduced directly with the harness's own environment, the build succeeds: exit 0, `npm install` clean, every package built. The earlier failure was transient and my attribution to a stale PATH was wrong — `/bin/sh` resolves here and always did. The probe now also runs the emitted binary's named lookup for `Git.Commit`, a component outside core's own documentation file, and compares it byte-for-byte with the source CLI. **A JSR consumer actually runs.** SYN47 stages core and its siblings outside the workspace, writes a consumer with an import map of its own that names no path in this repository, and asks it for two components' documentation. Listing the asset in a dry run proves it is in the payload; only this proves a consumer can load it. Verified discriminating: resolving the asset from the process working directory instead of the module URL fails it. * 🧾 Cancel inside the index, and stage what publish actually selects (#758) **SYN46 suspended in the wrong operation.** It stood in catalog discovery, which runs before the documentation work and proves nothing about it. There was no seam inside index construction to stand in, so this adds one: a module-private asset reader in `component-documentation.ts`, substitutable only through the source module and deliberately absent from `mod.ts` — not a provider, not a Context, not a package hook, so nothing a document or an installed package reaches can replace it. SYN46 now suspends while the named lookup is reading the packaged asset, with the catalog already built and the durable operation already open. Bypassing index construction makes it fail. **SYN47 was proving less than it claimed.** A recursive copy of a source directory would pass even if the publish filter dropped every asset, so it was not publication evidence. It now runs `deno publish --dry-run` for each package, asserts the assets are in what the filter *selected*, and stages exactly those files — so a filter that excluded an asset fails at staging rather than being copied around it. The consumer writes ordinary XMD and invokes the public `<Syntax names={…}>` surface rather than calling the index directly, and its complete rendered output is compared with the same program resolved against the workspace source: one document, one profile, two resolutions, so the only thing the comparison can differ on is the distribution. The casts are gone; both manifests parse through validated schemas. **One exclusion I nearly got wrong.** I excluded `documentation-validation.test.ts` from Node and Bun on the reasoning that its subject is a Deno entrypoint. It is not: the test spawns `deno run` as a subprocess, which works from any runtime, and it passes under Bun. Excluding it would have lost portable coverage for no reason, so the exclusion is removed and only the genuinely Deno-specific JSR probe is excluded — verified by running both files directly under Node and Bun. * 🧵 Give each execution its own packaged-asset reader (#758) The seam I added for SYN46 was a module-scoped mutable variable with an exported setter. One variable shared by every execution in the process: two runs would read through each other's reader, and substituting one changed what an unrelated execution was told the product says. A test-only intent does not make shared mutable state execution-local. The reader is now a value. It travels from where the execution is built — `runInvocation` → `invoke` → `executeDocument` → the observation — and the observation holds its own, so narrowing carries it and two executions in one process cannot reach each other's. There is no setter, no Context, no provider, no installation field and no hook: nothing a document, a component or an installed package can name reaches it at all. Production still uses the direct Effection filesystem. `executeReadingAssetsWith()` replaces `withAssetReader()`: it *builds a new execution* around a reader rather than changing anything an existing one holds, and is exported from `src/execute.ts` alone — not from `mod.ts`, not from `host.ts`. Importing it from a repository component gives you the ability to start your own execution, which you already had; it gives you no way to touch the current one's reader. SYN48 is the control that makes this a fact rather than a claim: one execution suspended inside documentation-index construction, a second ordinary execution overlapping it in the same process. The ordinary one reads canonical documentation and completes independently. Restoring the module-global implementation makes SYN48 fail, which is the check I should have written the first time — SYN46 alone passes under both designs. * 🚑 Authorize the root import only where a tier closes it (#758) CI found a regression the whole local battery missed. The protected tier is present in every execution, so `ExecutionImports` is now built unconditionally — it used to be `undefined` for a run with no bundle and no declarations. The root import read that as permission to authorize: imports === undefined ? imported : imports.authorize("__root__", imported) With the authority always present, that asks a question nothing answers — `__root__` is claimed by no tier unless a bundle closes the execution — and an ordinary run's root refuses with *this execution authorizes no import of this name*. It now asks only when a tier actually closes the name, which is the rule every other import already followed, and restores the previous behaviour exactly: absent authority and unclosed name both skip authorization, a bundle still closes the execution and still authorizes its root. Also restores the `documentation-validation.test.ts` exclusion I removed last round. My reasoning then was that the test shells out to `deno` and so is portable, and it does pass under a local Bun — because a developer machine has Deno installed. The CI Bun shard does not: every case fails with `Executable not found in $PATH: "deno"`. Needing the `deno` executable is exactly as disqualifying as calling `Deno.*`, and the local pass was the misleading signal. Verified: `deno task test packages/core/tests/` — 352 passed, 0 failed. * 🎯 Resolve one root catalog per occurrence, and document what a child registers (#758) **A named root occurrence asked its catalog twice.** `rootCatalogObservation()` supplied `current` as both the reference and the executable operation, and `document()` called both — so one occurrence invoked the trusted catalog contribution twice. Wasteful, and worse than wasteful: the environment can move between the two calls, and an entry's metadata would then come from a different catalog than the availability printed beside it. The observation now takes the authoring catalog and an *optional* admission. At a root there is no admission, so one resolution answers both questions. Under narrowing the two are genuinely different values, and the bare form reports the admission without asking the enclosing catalog at all. SYN49 proves it against a contribution that changes between calls — one occurrence, one call, both decisions from that value — with two occurrences still independent and a continuation restoring the retained text without asking again. **A nested `<Execution host="run">` was told less than it could run.** `testing-host.ts` installs the run profile's registrations and its `<Plan>` declaration, but passed no documentation, so a child's index held core's contributions alone: `<Syntax names={["WebForm"]} />` rendered the entry for a component the child can execute and then said it was undocumented. The contributions now travel beside the declarations they belong to. Audited the other host assemblies. `upgrade.ts` and `authorship-profile.ts` register no run-profile components, and `plan-component.ts` installs the registry for *validation* rather than an execution, so none of them can drift this way. `cli.ts` and `testing-host.ts` are the two run-profile executions, and both now assemble registrations and documentation from `useRunProfileRegistry()` and `runProfileDocumentation()` side by side. The nested regression names its fixture `lookup.md` rather than `webform.md`: this filesystem is case-insensitive, so a document of that name is found as the repository component `WebForm`, shadows the registration, and makes the fallback correct — the case would have been measuring the wrong thing and passing for it. Built on e90fe2e rather than d9985f1: that commit is the CI fix for the root import, which this keeps. * ♻️ Settle Syntax terminology and bootstrap documentation `SyntaxCatalog` becomes `SyntaxSymbols`, `CatalogObservation` becomes `SyntaxReference`, and the reference answers `symbols()`, `documentation()` and `available()`. The old names are removed rather than aliased: there are no users to migrate, and two names for one thing is how the two documentation lists drifted in the first place. Documentation now composes with the components it describes. Each package's bootstrap installs its registrations and its documentation in one call, through the additive `Documentation` Api; canonical core is the terminal, and every wrapper delegates before appending. Canonical execution collects once, after the trusted host's bootstrap and before the root import, and snapshots by value — so middleware a running document installs composes into a chain nothing reads, two contributions for one component refuse whichever order they were bootstrapped in, and sibling scopes stay isolated. That replaces the central `runProfileDocumentation()` list and the `ExecutionInstallation` `documentation` field, which a host had to keep in step with its registrations by hand and did not: a nested run registered `<WebForm>` and reported it undocumented. `xmd syntax` enters the same declarative bootstraps rather than splicing their registration arrays, so the command that has the components has the words that describe them. * 🚑 Carry the settled Syntax terminology into the distribution suites Three suites assert against a built artifact rather than the source tree, so none of them typechecks against `Syntax.ts` and none was in the local matrix. All three still held the previous wording or the removed installation field. The staged JSR consumer now bootstraps with `useAgentComponents()` instead of handing `executeInstalled` a `documentation` field that no longer exists. That field was silently ignored — the generated consumer is a string, so nothing typechecked it — and the consumer rendered `<Prompt>` as undocumented. The replacement is one call, which is the point of the bootstrap being one thing. The npm-binary and compiled-binary suites assert the approved description, which changed with the rename.
|
Architecture amendment: |
* 🚑 Refuse a repeated bootstrap, and finish the Syntax vocabulary Collection refused two contributions for one component of one package only when they named different assets. An exact repetition — what a profile that called one bootstrap twice produces — passed. Whether the second call also installed a provider, a launcher or an execution policy is not a question this boundary can answer, and the assembly is wrong either way, so it refuses now. It has to refuse at collection rather than in the named form's index. A document that writes bare `<Syntax />` builds no index, and one that writes no `<Syntax>` at all builds no reference either, so deferring would let both run to completion on an assembly nobody validated. SYN25l proves all three document shapes refuse, with a single bootstrap as the positive control. architecture.md said the assembled contributions travel by value on the execution installation, which stopped being true when the installation field was removed. It now states what is implemented: bootstraps install additive namespaced middleware, core is the terminal, and execution collects once after trusted bootstrap and snapshots by value before the root import. The rest is vocabulary the rename left behind — the `xmd syntax` broken-pipe diagnostic an operator actually reads, the `--json` help text, comments in `plan-component.ts` and `protected.ts`, the spec's "lexical observation", and the test helpers and authored bindings in the Syntax suite. Uses of "observation" that name the error-observation boundary, the document-target catalog and the diagnostic catalog are other product concepts and are left alone. * 📝 State the documentation ownership boundary the implementation has The Syntax section still said contributions "travel by value on the execution installation", describing the `ExecutionInstallation.documentation` field #758 removed. It now states what is implemented: a package bootstrap installs its registrations and additive `Documentation` middleware together, canonical core is the middleware terminal, and canonical execution collects the composed contributions once after trusted bootstrap and snapshots them by value before the root import. That captured snapshot — not a field on an installation — is what `xmd syntax` and an authored `<Syntax>` both consume. The duplicate rule is stated where it is enforced: any second contribution for one owning package and component refuses during collection, an exact repetition included, because collection is the only boundary every execution passes through. SYN25l joins the Tier SYN table with the three document shapes it covers and its positive control. The rest finishes the vocabulary audit this feature promised, in the focused specification section and the Syntax and `xmd syntax` test titles, helpers and comments. Uses of "observation" that name the import chain's own observing, the document-target catalog and the diagnostic catalog are other product concepts and are untouched. No runtime behavior changes. * 🚑 Let a package be bootstrapped at more than one layer Refusing every second contribution broke `xmd workflow`. The repository-composition vocabulary is deliberately installed twice — once by an ordinary run's bootstrap and again inside the workflow attachment, because either may be the only one — and the attachment's scope descends from the run's, so both wrappers sit in one chain. Collection saw two identical contributions and refused, and seven CI shards failed on it. `installation.ts` says so in its own words: "One array, three consumers." The repeat is the product's layering, not a wrong assembly, and an identical contribution cannot change what a document is told — which is the thing the refusal exists to protect. So a bootstrap that finds its own statement already in the chain does not append it again. Two contributions are the same statement when they agree on the owning package, the asset and the exact set of components; differing in any of the three makes them two statements, and two statements about one component still refuse at collection. SYN25l now proves the repeat is idempotent — the named form, the bare form and a document with no `<Syntax>` all work, and the collected snapshot holds the contribution exactly once — with two bootstraps naming different assets as the control that keeps it from passing vacuously. The specification and architecture.md say the same. * ♻️ Compare a documentation contribution by all four of its values Coalescing on owner, asset and component set left the documentation text out, so two bootstraps naming one path and reading different bytes — a stale build tree, a substituted reader — would have coalesced silently and kept whichever ran first. Equality now covers all four values a contribution is: owning package, asset identity, exact text, and component-name set. By value, so a fresh object each ask and a name set built in a different order are still the same statement. What reaches collection is therefore a real disagreement, and its refusal says which of the three differs rather than guessing. Two owners documenting a same-spelled component still coexist, because documentation joins by name and origin; so do disjoint sets from one owner. SYN25l becomes five rows: value-identical repetition adds nothing for the named form, the bare form and a document with no `<Syntax>`, with one bootstrap capturing exactly what two capture; a changed asset, text or overlapping set refuses in either order; distinct owner and disjoint set are the controls that keep that from over-refusing; overlapping executions stay isolated; and a layered trusted bootstrap keeps every layer's registrations *and* one documentation value. The specification and architecture.md state the four values, the coalescing rule, and the two non-conflicts. * ♻️ Classify duplicate documentation at collection, not in the helper `Documentation` is public. A package may compose `around(...)` itself and hand back two value-identical contributions without going through `contributeDocumentation()`, and that assembly is exactly as valid — but classification lived in the helper, so collection saw two contributions for one owner and component and refused. Correctness depended on which spelling a package happened to choose. Collection now owns it: snapshot the composed chain by value first, fold every completely identical contribution to one, then classify what is left. The helper appends and nothing more, so there is one rule in one place rather than two that can drift. Non-identical contributions overlapping one owner and component still refuse before root execution in either order, and a different owner or a disjoint set is still no conflict. SYN25l.6 installs two direct middleware wrappers that never call the helper — fresh objects, the same four values, name sets in opposite orders — and requires the named form, the bare form and a no-`<Syntax>` root to work with one contribution captured, then requires the three disagreements reached that way to refuse in both orders. Moving classification back into the helper fails that row and passes every other one in the tier.
…rofile Foundation for #713's protected <Evaluate>, not the surface itself. Six things the amended contract needs before a component can be built on them. The retained record is versioned. Version 2 tags itself at both the result and the policy level and carries the Workspace basis as one optional member, because an ordinary host evaluates against no Workspace and a reader that treated a missing basis as an empty one would compare those two equal. The untagged #369 record stays readable as version 1, proved against frozen literal records rather than version-2 records mechanically rewritten — a rewrite proves only that the rewriter and the reader agree. Both versions are now closed at every level: policy, Workspace basis, allowed entry, named invocation, and request. A member this build does not know means a record written under rules it does not have, so reading the members it recognizes would admit a grant on terms it never saw. `parseRequestRecord()` is the wrong reader for a retained ceiling — it ignores unknown members and treats a malformed timeout as an absent one — so requests read through a closed reader here instead. Exact text joins the ceilings a continuation is held to. An admission is a decision about one fragment; a caller now holding different text is asking for a decision nobody made, so it refuses rather than silently expanding the retained copy. GX24/GX24b asserted the superseded behavior and now assert this, with GX24c as the unchanged-text control. A Workspace basis is validated where it is stated, not only where it is compared: non-empty unique roots and a selected root the host retains. A basis with no roots or a selected root nothing holds is not a stricter grant, it is nothing for a continuation to be held to. `ExecutionInstallation.evaluation` carries one host profile, refused in the plural on the same terms as `symbols`, and reaches canonical expansion on the private authority. The deep by-value capture, the per-entry request ceilings and the captured provider operations are the next commit; this one places the singleton and its carrier.
Advisories 4, 5 and 6 on the profile foundation. Still not the surface.
A host now states `FragmentEvaluationInput` — entry tables, an optional
Workspace snapshot operation, its resolved Fetch timeout, and whether it accepts
the released `source` spelling — and canonical execution captures a profile of
its own from it before any `install()` runs. Every structural value is
deep-copied and frozen and every live operation is read and bound once, so a
host that edits its own arrays, schemas, headers or roots afterwards is editing
objects nothing is looking at. The previous `Object.freeze` around the outer
array said by-value and was not.
An entry binds `{origin, key, revision}` rather than one opaque string, because
a continuation compares it and a reader has to be able to say which part moved.
It is never derived from a function: an implementation is not an identity.
Request ceilings belong to the entry that owns them. Flattening them would let
one entry's limit admit another entry's request, so each entry normalizes,
deduplicates and canonically orders its own — against the timeout the host
resolved when it built the profile, through `normalizeFetchRequest()`, rather
than a context read at whichever point the comparison later happens.
The Workspace is an operation rather than a value because a run's own progress
advances it: a basis captured at installation is stale by the second evaluation
in one run. Canonical Evaluate will ask per invocation. An ordinary host has
none, which stays distinct from having an empty one.
Only the host input type is exported, and only from `core/host`, beside the
`ExecutionInstallation` it belongs to. The captured profile and its bound
operations are unexported: ordinary core publishes no getter for active
authority and no way to install a provider.
`<Evaluate>` was a registration the workflow host declared, so what an
unaudited fragment is allowed to do depended on which package answered a
name. It is now canonical core's, in the protected tier beside `<Syntax>`,
and a host states only the ceiling.
- `Evaluate.ts` is the protected component: two disjoint input forms
(`text`, or paired content that produces the program), `allow` selecting
among the host's tables, and the result binding observations beside the
fragment's rendered output.
- `syntax-admitted.ts` describes what a fragment may write, so a producer
rendered inside the paired form is told the narrowed vocabulary rather
than the enclosing site's.
- Canonical expansion delivers a one-shot content projector directly to a
protected body. Only the ordinary handle is published through
`ActiveProjection`.
- Core's entry constructors (`fileReadEntry`, `fileWriteEntry`,
`fileDeleteEntry`, `fetchEntry`) carry structural `{origin, key, revision}`
identities, so a changed grant refuses a continuation instead of widening
it silently.
- `xmd run` and its run children state a Files-only profile; the workflow
attachment states its Workspace-bound one and keeps the released `source`
spelling working.
The workflow's `evaluationComponents()` is gone: nothing registers this name.
An admitted `<File />` was core's ordinary component, which resolves `API.Files` when it runs. So the authority a generated fragment exercised was whatever the document, a repository component or middleware had installed by then — and a handler composed around one could observe, rewrite or answer a fragment's read. A profile now states operations rather than definitions. A host names a capability; `captureEvaluationProfile()` reads each method off the host's object once, binds it behind a revocation the execution owns, and closes core's own body over the result. A fragment reaches those five file operations and one transport, and nothing else: not the rest of the host's provider, not `API.Files`, not `API.Fetch`, not `API.Env`. - `fragment-capabilities.ts` holds the bodies and the bound operations. One name resolves to one definition whose dispatch separates its spellings, as the ordinary `<File>` does. - A directory scopes its content through the evaluation's own cursor, not the contextual environment, so nothing outside a fragment can move where its content writes. - An admitted request is journaled through the same `persistFetch` an authored `<Fetch>` uses and answers with the same record. - Core identities go to revision 2 and `Dir` to 3: the authority behind them narrowed, so a continuation granted under the composable one is refused rather than re-granted. - Operations are revoked at execution teardown, registered before the first `install()`. FE13 proves the separation, with the document's provider installed nearer and throwing if reached. FE3 proves the producer projection bypasses `content`, `tryContent` and `hasContent`, with an ordinary component in the same run as a positive control that the handlers are live.
The component and its captured capabilities were already green; what was missing was evidence for most of the contract and prose that still described the workflow registration this replaced. Evidence, in three new homes plus the existing core tier: - core `evaluate-component.test.ts` gains the constructs a fragment may not carry, `as` and its absence, two occurrences taking two durable names, exact-text and moved-ceiling continuations truncated to the admission, an unreadable retained record, and a document binding every composable channel it has before an `<Evaluate>` beneath it. - core `evaluate-loaded-copy.test.ts` separates ownership from authority: the name cannot be registered, a middleware answer is refused however honest, honest delegation and a deliberate refusal both still work, a replacement written to ignore `allow` is never entered, and a repository `File` wins at an authored site while still not entering a fragment. - cli `evaluate-component.test.ts` drives real `xmd run`, and reads the source symbols for the protected origin and approved description. - cli `evaluate-workflow.test.ts` is Deno-only against a real run database: `source` and `text` behave identically there with no warning, `program` is a spelling no profile has, and the fragment reaches the run's Workspace rather than the caller's disk. Two continuation rows needed the document to be byte-identical across attempts, because the occurrence identity is part of the durable name — a changed document makes a different record rather than a stale one, so a producer that renders differently is what actually reaches the refusal. Prose: architecture and both specs now describe a protected component whose host supplies a ceiling rather than an implementation, entries that name capabilities rather than definitions, the two disjoint input forms, and the one-shot projection. New §5.3.2 and the Tier FE acceptance table. The npm boundary asserts `<Evaluate>` explicitly, because `--changed` cannot discover it: nothing in the emitted package shares a path with the core sources the name is defined in. Compatibility: core entries state revision 2 and `<Dir>` revision 3. A run suspended under the earlier revisions refuses to resume rather than silently receiving the narrower grant.
The one-shot projector was two booleans inside `expand.ts`, and its guards were unreachable from a document: only canonical `<Evaluate>` consumes a projector and it consumes one once, so no black-box test could make a second call, retain a callback past a body, or race two. A test that could would need a protected component of its own, which is a hole in the tier the projector exists inside. So the state machine is its own package-internal module. `expand.ts` keeps what to project, under which authority and in which scope; the lease keeps how many times it may answer and for how long. Three explicit states rather than two flags, and consumption happens before the underlying operation can suspend — a machine that marked itself spent on completion would let a concurrent call project the same content twice. `protected-content.ts` is imported by `expand.ts` and its own unit test and by nothing else: no barrel, no host surface, no package export, no Context, no Component api, no test-support. Tier PC proves the machine directly, counting the underlying operation rather than only the error: first call exact and second refused with one run; a closed lease never constructing the operation; a retained callback refusing; a concurrent call during a suspended first; spent after failure and after cancellation; two independent leases; and idempotent close. Integration evidence stays where it belongs. FE2 now pins the admitted source to the exact projected bytes — the content region's own newlines included, nothing trimmed and nothing added — and FE17 cancels inside a live producer, waits for its cleanup, and proves no admission and no fragment operation, with an early-halt negative control beside it.
FE11 replays a completed evaluation over its whole retained history. The second run's recorder holds different bytes under the same path, so a re-read would show up in the rendered output rather than only in a count — and the output is identical to the first run's while the recorder's log stays empty. FE28 gains the compiled boundary beside the npm one, in the suite that already asks `dist/xmd` what it carries. Both are asserted explicitly because `--changed` cannot see either: nothing in the emitted package or the binary shares a path with the core sources the name is defined in, and no test shard builds the binary at all. The closed schema travels with it, so a build shipping a widened one fails here.
The combined vocabulary row could not discriminate. `<File>`, `<Evaluate>` and `<Loop>` all appear in Plan's fixed instructions, so a row asserting on one of them passes on prose rather than on the catalog — and the earlier attempt did exactly that. FE8 and FE9 are now two tests over two invented identities that occur nowhere else: `FE713.Read` (admitted) and `FE713.WideOnly` (available at the authored site, absent from every profile), each carrying a marker no other text in the repository contains. A `RememberSyntax` producer-site component captures what the public `<Syntax />` rendered without it becoming part of the program. FE8 nests the Plan inside `<Evaluate>`: the captured catalog and Plan's first prompt both name the admitted entry with its origin, the marker appears exactly once, the wide-only marker never appears, and the fragment runs once. FE9 defers it: the catalog is the authored site's, and the later narrower `<Evaluate>` refuses the whole fragment for the wide-only element even though the admitted read is written first — the empty recorder is what proves preflight decided the whole fragment. Along the way this found a real gap. A narrowed catalog named its entries and said nothing about them, because the description was read off the capability body rather than the host's entry. `FragmentEntry` now carries one and core's own entries state theirs, so an agent told it may write a name is also told what the name does. Also: the lease's `SyntaxReference` is now a structurally valid inert reference rather than two casts, its rows are `PCL` because `PC` is the Plan suite's, and FE17 gains its second half — cancelling after the admission committed and an admitted read is in flight, which the producer row could not reach.
A generated fragment's admission decides *which implementation* may run, and a continuation has to be able to tell that the implementation behind a name is still the one it was admitted with. A function carries no identity a run can retain — comparing one compares how somebody wrote their code — so the identity has to be stated. `AnswerIdentities` is where a provider states it. The claim is keyed by the exact object being returned, so an outer handler that replaces the answer returns a different object carrying no claim, and one that edits the claimed object fails the retained comparison. Nothing travels on the definition, so a claim cannot be read off one, copied onto another, or forged by describing it. The claimant is minted per execution and revoked with it. This is deliberately not authority. An unidentified answer stays a perfectly good answer for ordinary expansion; what it cannot be is what a fragment runs, because there would be nothing for a continuation to compare. Tier FE15 proves the lifetime: identity read back off the exact answer; a structurally identical object and a spread copy carrying nothing; an edited claim invalidated; a retained claimant refusing after teardown and its earlier statements identifying nothing; two executions independent as the positive control; a partial identity refused rather than half-recorded.
…713) The identity table was a second WeakMap beside `CanonicalImports`, which already holds issuance, retention and the `stillDescribes` comparison. Two registries are two places an answer can be authorized from, so the claim now lives in that owner: one table, one lifecycle, one comparison. The claimant is minted per provider installation and carries that installation's origin, so a provider states only key and revision and cannot assert another provider's origin. It is an ordinary closure with no shared symbol, module registry or context name behind it, so a separately loaded copy can hold one without any of them being a way in. The owner starts inactive: canonical execution registers teardown, then activates, then mints — a claim landing before that would be a claim with no teardown behind it. Claims are idempotent for the same claimant, name, key and revision, and a competing claimant, name, key or revision refuses without overwriting the first. An overwrite would let a second provider rename the first's implementation. Also removes a raw NUL byte from `evaluation-profile.ts`, in a dead `const key` line left behind when its use was replaced. The NUL is why several Edit attempts could not match that line, and why `ugrep` reported no matches in the file — it was treating the source as binary, and I read that false negative as the line already being gone. `deno check` and the suites passed throughout because the byte sat inside a template literal that was never read.
A host can now say one of two things about what is behind an admitted name, and the type is closed so a third answer cannot appear later: a third shape would be a third answer to "where does this implementation come from", which is the question an admission has to settle once. `capability` is unchanged — core supplies the body, bound to the host's own operations, and no lookup happens at fragment time. `component-answer` is new: the host states the name, the structural identity a provider must have claimed for it, and the forms, and states no implementation at all. There is no definition, function, resolver, provider operation or context handle on that arm to put one in. Props and the callable definition are derived from the exact final import answer instead, so a host cannot describe a contract the implementation does not have. `ComponentAnswerInstallation` is the host-facing seam. Canonical execution runs it during profile capture and hands it a claimant fixed to that installation's origin, so a provider states a key and a revision and cannot assert another provider's. It is an ordinary closure — no shared symbol, module registry or context name — which is what lets a separately loaded copy hold one without any of those being a way in. `CapturedEntry` keeps `kind`, because the retained record does: a continuation comparing identities has to know whether what stood behind a name was core's own operation or a provider's answer, and those are different grants. Type-level only so far; behavior is unchanged and every existing row still passes. The capture step that resolves and reconciles these entries is next.
Architect review of the eager-capture tree found six ways a provider-backed entry could be weaker than it looks, plus two documentation gaps. A profile's `component-answer` entries were last-write-wins by name, so two entries under one name silently resolved to whichever the host stated last. One name is one component: the two spellings share one lookup and one sealed implementation, and a second identity — or a name held as both a capability and an answer — refuses at capture rather than letting assembly order decide. Identification handed back an identity and left the caller to copy the chain's object itself, which is a second read of a value the chain controls. It now answers with the claim *and* core's own claim-time copy in one result, and capture seals that copy without reading the answer again. The run's ceiling table joined the four identity terms with a space, so two identities differing only in where a space falls shared one entry's limit. It is encoded rather than joined. FE18 refused every version-1 record for having a string where version 2 has a record. A version-1 identity is reconciled under the `origin#key@revision` mapping the build that wrote it used, and only against a capability — the component-answer arm did not exist then. FE14 now composes two live providers rather than asserting about one that was never in the chain, and its continuation pair supplies both the unchanged A and the changed B so neither running is a fact. FE15 exercises a losing claimant while the winning execution is still live, all three teardowns, and two overlapping executions. FE30 layers the provider bootstrap itself through an inherited and a local entry. `<Fetch>`'s tier is renamed FE to FT throughout, so `FE14` names one row.
Three corrections to the previous round, with everything else preserved. The version-1 reconciliation was fabricated. `bb2c1c49` has no `evaluation-profile.ts` and no core `<Evaluate>`, so `origin#key@revision` was never written by any released build. What those journals actually hold is one opaque value chosen by whoever built the pinned entry: `@executablemd/core#Fetch`, `#File:read`, `#File:write`, `#File.Delete`, the workflow host's `@executablemd/workflow/composition/dir-v2#Dir`, and whatever string a host passed to `pinnedComponent`. Nothing recovers four structural terms from those, so nothing infers. A capability entry now states the exact version-1 strings it succeeds — core states its own, the workflow host states Dir's — snapshotted by value at capture, and a retained string reconciles against that list and nothing else. An entry that states none refuses the record that named it. A component answer has no alias field, because that arm postdates every version-1 record. The positive controls are literal records `bb2c1c49` emitted: the standard core read admission, and an arbitrary host identity that resembles no structural identity at all. Claims were owner-active for the whole execution, so a losing provider could record long after its decision was made — and the previous round's test asserted that as correct. A claim window now opens for the exact resolution occurrence and name and closes in `finally` on success, fallback, failure or cancellation. A retained losing claimant refuses while the execution continues, and a claim held from one settled resolution cannot land in the next one under either name. The Tier FE matrix is restored to the handoff's exact FE1–FE30, with FE8, FE9, FE11, FE17 and FE22 back and no invented row. The provider, reconciliation and profile coverage those rows summarize moved to GX and CIV, where the implementation tiers belong.
Three architecture blockers, and one correction to the first attempt at the first of them. A claim was admitted on the strength of whichever window happened to be open plus its name, and the Claim recorded no occurrence at all. So a claim left over from a decided import could answer a later one. The claim now records the occurrence it was stated in, and identification — which canonical execution performs inside the window — refuses a claim belonging to another. A stale occurrence lease is unusable in the next occurrence. The first attempt at that leased the whole installation claimant to its first occurrence, which was wrong: one `ComponentAnswerInstallation` owns one origin and may legitimately answer several admitted names and repeated resolutions, and the settled contract asks for no installation per component. What settles is the opportunity, not the claimant — a resolution takes one statement from a claimant and the next resolution offers it a fresh one. Both scales carry a positive control where one installation identifies two distinct admitted names. `coreLegacy(key)` assembled version-1 aliases from the origin and the key, which is the inference this design exists to refuse. The four core strings and the workflow Dir string are enumerated literally at the entries that state them, and every released alias — Fetch, File read, File write, File.Delete and the workflow Dir — has a literal version-1 continuation control. Arbitrary aliases stay host-supplied, component answers stay alias-free, and new writes stay structural version 2. The Tier FE matrix is the handoff's FE1-FE30, verbatim and two-column, rather than a reauthored expansion. The elaborated provider, reconciliation and profile evidence stays under GX, CIV and the FT Fetch tier.
A stable installation claimant could not prove which invocation was speaking.
It only ever established "some provider this host installed", so a handler that
had already returned — or one that lost a decision — could state an answer to
an import it was not deciding, and the owner had no way to tell that apart from
the honest case. Limiting statements per occurrence narrowed the window without
closing it.
So the two authorities are now separate objects. An installation receives a
registrar and never a handle it can claim with; the registrar composes import
middleware, and canonical execution mints one request per invocation of it.
A request fixes the asked name and a by-value copy of the asked position,
captures the exact live `ResolutionWindow` by identity, and is the only thing
that claims — `claim(answer, {key, revision})` takes no name, so a handler
cannot answer for a component it was never asked about. The wrapper closes the
request synchronously in `finally` on return, failure or cancellation, while an
outer request stays live across `yield* next()` and may claim its replacement
after the delegated handler returns.
Identification now takes the window explicitly and accepts only a claim
recorded for that exact object under that exact name, so provenance is never
read out of whichever window happens to be current.
The installation stays reusable, which is the property the last round broke:
one provider answers several admitted names and repeated resolutions through
distinct requests, and the per-window spent table is secondary — it only keeps
one provider from naming two implementations for one import.
Registration is spelled `around`, matching `Component.around` rather than
inventing a second vocabulary for middleware a host already composes.
Unidentified middleware replacements remain ordinary valid import answers
outside fragment evaluation. No Context, shared symbol, public brand or
module-global registry participates.
|
Architect review: PASS on signed feedback commit Independent verification:
Delivery head CI is now the remaining delivery gate. |
CI caught one stale row rather than a lost invariant. At the merge base the workflow run's third installation declared `<Evaluate>` as an identity component. #713 moved that component into canonical core's protected table and put the fragment-evaluation profile in the same slot, so a run now hands core three kinds of installation: the run contract with its admission and preparation, the bundle with its own admission, and a profile that carries a ceiling and no admission at all — because stating what a generated fragment may do is not a claim about this run's history. WFI1 modelled installations as two kinds and required everything without `components` to carry exactly one admission, so the profile read `undefined`. The invariant the row is named for never moved: the run-contract installation still carries its admission and its preparation, and that half passed. Correcting the row rather than loosening it. It now recognizes all three kinds, asserts positively that exactly one installation states a profile and that the profile has a non-empty read table, and asserts that nothing declares `<Evaluate>` — the assertion that replaces the one about the mechanism #713 removed. Dropping the profile, emptying it, or re-declaring the component each fail the row. `core/host.ts` said the installer is handed a claimant. It is handed a registrar, and the request per middleware invocation is what states an answer.
Why
Generated XMD text needs an explicit execution boundary.
<Plan>produces inert text;<Evaluate>is the protected component that validates and runs a constrained fragment.What changed
<Evaluate>with disjointtextand paired-content forms.{ observations, output }throughas; fragment output is not emitted into the enclosing document.allowasread. Read and write are separate exact capability tables; write does not imply read.returns, and independent<Output>selection before fragment effects.<Syntax>and a directly nested<Plan>to the admitted fragment vocabulary.sourceprop as a silent deprecated alias;textis canonical and ordinary runs rejectsource.generated_xmdrecord to closed version 2 while reconciling only the exact literal version-1 aliases each capability owner declares.Approved component description:
Authority boundary
<Evaluate>is protected like<Syntax>: repository files, workflow bundles, declarations, registrations, import middleware, document context, and another loaded package copy cannot replace it.Protection selects the policy interpreter; it grants no capability. The trusted host supplies one private fragment profile before document code. Canonical execution snapshots it by value, applies
allow, resolves provider answers before root import, and evaluates only the admitted identities and forms.Provider identity is request-bound rather than held by a reusable claimant. The request fixes the asked name and a by-value source position, captures the exact live resolution-window object, and closes synchronously when its handler returns, fails, or is cancelled. An outer request remains live across delegation and may claim its replacement after the inner handler returns.
No Context name, shared symbol, public brand, definition field, function-identity inference, or module-scoped registry carries this authority.
Evidence
Feedback implementation:
ace5484a9e7aeb28f9f0ca745969d7dc4261533d(signed, verified fortarasm@gmail.com).deno task check,deno task lint, andgit diff --check: exit 0a8a586b4c10583c8243e645de43ce3833c56a4eahas the approved commit as first parent and the former PR head as second parent; its tree is byte-identical toace5484a. This reconciles published history without a force-push.CI delivery checks are running on the pushed head.
Compatibility and risk
ComponentAnswerClaimis replaced byComponentAnswerHandler,ComponentAnswerRegistrar, andComponentAnswerRequest. No in-repository consumer remains on the former host API.sourcealias remains accepted for released documents but is omitted from new examples.Scope
Included: constrained XMD fragment evaluation in ordinary and workflow runs, public Syntax narrowing, provider-backed identities, and durable replay.
Unchanged:
<Plan>only produces text; structural constructs use structural dispatch; ordinary component defaults remain replaceable; there is no<Program>,<Source>, or<Execute>component.Closes #713.